Skip to main content
Glama
northernvariables

FedMCP - Federal Parliamentary Information

page.tsx9.76 kB
/** * Chamber View Page - 2x2 Grid Layout * Fully bilingual with Quebec French support * * Optimized layout with: * - Live stats bar at top * - Top Left: Video player * - Top Right: Live transcript * - Bottom Left: Video playlist * - Bottom Right: Floor plan with filters */ 'use client'; import { useQuery } from '@apollo/client'; import { useState } from 'react'; import { useSearchParams } from 'next/navigation'; import { useTranslations, useLocale } from 'next-intl'; import { Header } from '@/components/Header'; import { Footer } from '@/components/Footer'; import { Loading } from '@/components/Loading'; import { Card } from '@canadagpt/design-system'; import { GET_CHAMBER_SEATING } from '@/lib/queries'; import { SeatingChart } from '@/components/chamber/SeatingChart'; import { CPACPlayer } from '@/components/chamber/CPACPlayer'; import { VideoPlaylist } from '@/components/chamber/VideoPlaylist'; import { MPModal } from '@/components/chamber/MPModal'; import { LiveStatsBar } from '@/components/chamber/LiveStatsBar'; import { PartyFilters } from '@/components/chamber/PartyFilters'; import { TranscriptPanel } from '@/components/chamber/TranscriptPanel'; import { TheaterMode } from '@/components/chamber/TheaterMode'; interface MP { id: string; name: string; party: string; riding: string; photo_url?: string; cabinet_position?: string; email?: string; phone?: string; seat_row?: number; seat_column?: number; bench_section?: string; seat_visual_x?: number; seat_visual_y?: number; } interface Debate { id: string; date: string; topic?: string; cpac_video_url?: string; video_duration?: number; } export default function ChamberPage() { const t = useTranslations('chamber'); const locale = useLocale(); const searchParams = useSearchParams(); const highlightedMpId = searchParams.get('mp'); const [selectedMp, setSelectedMp] = useState<MP | null>(null); const [isModalOpen, setIsModalOpen] = useState(false); const [selectedDebate, setSelectedDebate] = useState<Debate | null>(null); const [isTheaterMode, setIsTheaterMode] = useState(false); const [selectedParty, setSelectedParty] = useState<string | null>(null); const [searchQuery, setSearchQuery] = useState(''); const { data, loading, error } = useQuery(GET_CHAMBER_SEATING); // Mock debates data - will be replaced with real GraphQL query const mockDebates: Debate[] = [ { id: 'debate-2024-01-15', date: '2024-01-15', topic: 'Question Period', video_duration: 3600, }, { id: 'debate-2024-01-16', date: '2024-01-16', topic: 'Budget Debate', video_duration: 7200, }, { id: 'debate-2024-01-17', date: '2024-01-17', topic: 'Private Members Business', video_duration: 5400, }, ]; const handleSeatClick = (mp: MP) => { setSelectedMp(mp); setIsModalOpen(true); }; const handleDebateSelect = (debate: Debate) => { setSelectedDebate(debate); }; const allMps: MP[] = data?.mps || []; // Normalize party name to handle accent variations (Québécois vs Quebecois) const normalizePartyName = (name: string): string => { return name.toLowerCase() .normalize('NFD').replace(/[\u0300-\u036f]/g, '') // Remove accents .trim(); }; // Filter MPs based on party and search const filteredMps = allMps.filter((mp) => { // Handle party filtering with name variations const matchesParty = !selectedParty || mp.party === selectedParty || normalizePartyName(mp.party) === normalizePartyName(selectedParty) || (selectedParty === 'Bloc Québécois' && mp.party === 'Bloc') || (selectedParty === 'Green' && mp.party === 'Green Party') || (selectedParty === 'Green Party' && mp.party === 'Green'); const matchesSearch = !searchQuery || mp.name.toLowerCase().includes(searchQuery.toLowerCase()) || mp.riding.toLowerCase().includes(searchQuery.toLowerCase()); return matchesParty && matchesSearch; }); const cabinetCount = allMps.filter(mp => mp.cabinet_position).length; return ( <div className="min-h-screen flex flex-col"> <Header /> {/* Live Stats Bar */} <LiveStatsBar /> <main className="flex-1"> {loading ? ( <div className="page-container py-12"> <Loading /> </div> ) : error ? ( <div className="page-container py-12"> <Card> <p className="text-accent-red">{t('errors.loadingData')} {error.message}</p> </Card> </div> ) : ( <> {/* Left-Right Split Layout: Video/Transcript Stack | Floor Plan/Playlist Stack */} <div className="grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-6 max-w-[1920px] mx-auto px-4 sm:px-6 lg:px-8 py-6"> {/* Left Column: Combined Video + Transcript */} <div> <Card className="overflow-hidden"> {/* Video Player with Overlay Playlist */} <div className="relative"> <CPACPlayer title={selectedDebate?.topic || t('video.questionPeriod')} date={ selectedDebate?.date ? new Date(selectedDebate.date).toLocaleDateString(locale === 'fr' ? 'fr-CA' : 'en-CA', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', }) : new Date().toLocaleDateString(locale === 'fr' ? 'fr-CA' : 'en-CA', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', }) } isLive={false} videoUrl={selectedDebate?.cpac_video_url} compact={true} onTheaterMode={() => setIsTheaterMode(true)} noCard={true} /> {/* Playlist Overlay - Right 50%, starts below header, shows on hover */} <div className="absolute top-[52px] right-0 bottom-0 w-1/2 opacity-0 hover:opacity-100 transition-opacity duration-300 pointer-events-none group"> <div className="h-full bg-gradient-to-l from-black/60 to-transparent backdrop-blur-sm pointer-events-auto"> <VideoPlaylist debates={mockDebates} selectedDebateId={selectedDebate?.id} onDebateSelect={handleDebateSelect} /> </div> </div> </div> {/* Live Transcript - Fixed Height */} <div className="h-[300px] border-t border-border"> <TranscriptPanel /> </div> </Card> </div> {/* Right Column: Floor Plan */} <div> {/* Combined: Seating Chart + Filters */} <Card> <div className="p-4"> {/* Centered Title */} <div className="text-center mb-3"> <h2 className="text-xl font-bold text-text-primary"> {t('seating.title')} </h2> <p className="text-xs text-text-secondary mt-1"> {t('seating.instruction')} </p> </div> {/* Party Filters - Integrated */} <div className="mb-4 pb-4 border-b border-border"> <PartyFilters onPartyFilter={setSelectedParty} onSearch={setSearchQuery} selectedParty={selectedParty} searchQuery={searchQuery} /> </div> {/* Seating Chart SVG */} <div> <SeatingChart mps={filteredMps} onSeatClick={handleSeatClick} highlightedMpId={highlightedMpId || undefined} /> </div> </div> </Card> </div> </div> </> )} </main> <Footer /> {/* MP Modal */} <MPModal mp={selectedMp} isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} /> {/* Theater Mode */} <TheaterMode isOpen={isTheaterMode} onClose={() => setIsTheaterMode(false)} videoElement={ <CPACPlayer title={selectedDebate?.topic || t('video.questionPeriod')} date={ selectedDebate?.date ? new Date(selectedDebate.date).toLocaleDateString(locale === 'fr' ? 'fr-CA' : 'en-CA', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', }) : new Date().toLocaleDateString(locale === 'fr' ? 'fr-CA' : 'en-CA', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', }) } isLive={false} videoUrl={selectedDebate?.cpac_video_url} /> } /> </div> ); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/northernvariables/FedMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server